go-ethereum 源码笔记(cmd 模块-其他命令)

cmd 包中除了 ethereum cli 客户端还包括很多其他可执行命令。下面我们将一一分析这些子命令,与上一篇 go-ethereum 源码笔记(cmd 模块-geth 命令)一样,我们不会深入到其他模块中,这一篇仅限于 cmd 模块。

abigen

abigen 可以根据 sol 或 abi 文件生成特定语言的封装,支持 golang, objc, java 3种语言。它也能够编译 Solidity 原文件,使开发更便利。

ABI 指的是 Application binary interface,字面意思是应用二进制接口。它是与区块链以外的以太坊系统中的合约进行交互的标准方式,同时也是合约与合约交互的标准方式。

除了通过 RPC 的方式调用,部署合约,我们还可以使用 IPC 的方式,即使用 abigen 这种方式,使得部署合约更简单,更易于与代码集成。

可以写一个简单的 test.sol 文件试一下,通过 abigen --sol test.sol -pkg main --lang go --out test.go 先生成一个 test.go 文件,观察这个生成的绑定文件你会发现这个文件生成了与这个合约交互的所有方法,里面还包含了部署的方法,可以通过客户端来可以调用这些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
func main() {
flag.Parse()

if *abiFlag == "" && *solFlag == "" {
fmt.Printf("No contract ABI (--abi) or Solidity source (--sol) specified\n")
os.Exit(-1)
} else if (*abiFlag != "" || *binFlag != "" || *typFlag != "") && *solFlag != "" {
fmt.Printf("Contract ABI (--abi), bytecode (--bin) and type (--type) flags are mutually exclusive with the Solidity source (--sol) flag\n")
os.Exit(-1)
}
if *pkgFlag == "" {
fmt.Printf("No destination package specified (--pkg)\n")
os.Exit(-1)
}
var lang bind.Lang
switch *langFlag {
case "go":
lang = bind.LangGo
case "java":
lang = bind.LangJava
case "objc":
lang = bind.LangObjC
default:
fmt.Printf("Unsupported destination language \"%s\" (--lang)\n", *langFlag)
os.Exit(-1)
}
var (
abis []string
bins []string
types []string
)
if *solFlag != "" || *abiFlag == "-" {
exclude := make(map[string]bool)
for _, kind := range strings.Split(*excFlag, ",") {
exclude[strings.ToLower(kind)] = true
}

var contracts map[string]*compiler.Contract
var err error
if *solFlag != "" {
contracts, err = compiler.CompileSolidity(*solcFlag, *solFlag)
if err != nil {
fmt.Printf("Failed to build Solidity contract: %v\n", err)
os.Exit(-1)
}
} else {
contracts, err = contractsFromStdin()
if err != nil {
fmt.Printf("Failed to read input ABIs from STDIN: %v\n", err)
os.Exit(-1)
}
}
for name, contract := range contracts {
if exclude[strings.ToLower(name)] {
continue
}
abi, _ := json.Marshal(contract.Info.AbiDefinition) // Flatten the compiler parse
abis = append(abis, string(abi))
bins = append(bins, contract.Code)

nameParts := strings.Split(name, ":")
types = append(types, nameParts[len(nameParts)-1])
}
} else {
abi, err := ioutil.ReadFile(*abiFlag)
if err != nil {
fmt.Printf("Failed to read input ABI: %v\n", err)
os.Exit(-1)
}
abis = append(abis, string(abi))

bin := []byte{}
if *binFlag != "" {
if bin, err = ioutil.ReadFile(*binFlag); err != nil {
fmt.Printf("Failed to read input bytecode: %v\n", err)
os.Exit(-1)
}
}
bins = append(bins, string(bin))

kind := *typFlag
if kind == "" {
kind = *pkgFlag
}
types = append(types, kind)
}
code, err := bind.Bind(types, abis, bins, *pkgFlag, lang)
if err != nil {
fmt.Printf("Failed to generate ABI binding: %v\n", err)
os.Exit(-1)
}
if *outFlag == "" {
fmt.Printf("%s\n", code)
return
}
if err := ioutil.WriteFile(*outFlag, []byte(code), 0600); err != nil {
fmt.Printf("Failed to write ABI binding: %v\n", err)
os.Exit(-1)
}
}

abigen 命令的源码很简单,实际上就是调用 accounts/abi 以及 Solidity 编译器,主要的实现还是在 accounts/abi 里,这部分我们在后面的文章会继续深入。

bootnode

bootnode 是 geth 客户端的精简版本,只实现了网络节点发现协议,不运行更高级别的应用协议。它可以用作轻量级引导节点,帮助在私有网络中查找 peers。

以太坊在启动时至少需要一个对等节点,这样才能接入整个以太坊网络,bootnode 相当于一个第三方的中介,node 在启动时会将自己的信息注册到 bootnode 的路由中,并且会从 bootnode 得到其它节点的路由信息,一旦有了对等节点信息后就可以不需要连接 bootnode。公有链的节点硬编码了一些 bootnode 节点地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
func main() {
var (
listenAddr = flag.String("addr", ":30301", "listen address")
genKey = flag.String("genkey", "", "generate a node key")
writeAddr = flag.Bool("writeaddress", false, "write out the node's pubkey hash and quit")
nodeKeyFile = flag.String("nodekey", "", "private key filename")
nodeKeyHex = flag.String("nodekeyhex", "", "private key as hex (for testing)")
natdesc = flag.String("nat", "none", "port mapping mechanism (any|none|upnp|pmp|extip:<IP>)")
netrestrict = flag.String("netrestrict", "", "restrict network communication to the given IP networks (CIDR masks)")
runv5 = flag.Bool("v5", false, "run a v5 topic discovery bootnode")
verbosity = flag.Int("verbosity", int(log.LvlInfo), "log verbosity (0-9)")
vmodule = flag.String("vmodule", "", "log verbosity pattern")

nodeKey *ecdsa.PrivateKey
err error
)
flag.Parse()

glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(false)))
glogger.Verbosity(log.Lvl(*verbosity))
glogger.Vmodule(*vmodule)
log.Root().SetHandler(glogger)

natm, err := nat.Parse(*natdesc)
if err != nil {
utils.Fatalf("-nat: %v", err)
}
switch {
case *genKey != "":
nodeKey, err = crypto.GenerateKey()
if err != nil {
utils.Fatalf("could not generate key: %v", err)
}
if err = crypto.SaveECDSA(*genKey, nodeKey); err != nil {
utils.Fatalf("%v", err)
}
return
case *nodeKeyFile == "" && *nodeKeyHex == "":
utils.Fatalf("Use -nodekey or -nodekeyhex to specify a private key")
case *nodeKeyFile != "" && *nodeKeyHex != "":
utils.Fatalf("Options -nodekey and -nodekeyhex are mutually exclusive")
case *nodeKeyFile != "":
if nodeKey, err = crypto.LoadECDSA(*nodeKeyFile); err != nil {
utils.Fatalf("-nodekey: %v", err)
}
case *nodeKeyHex != "":
if nodeKey, err = crypto.HexToECDSA(*nodeKeyHex); err != nil {
utils.Fatalf("-nodekeyhex: %v", err)
}
}

if *writeAddr {
fmt.Printf("%v\n", discover.PubkeyID(&nodeKey.PublicKey))
os.Exit(0)
}

var restrictList *netutil.Netlist
if *netrestrict != "" {
restrictList, err = netutil.ParseNetlist(*netrestrict)
if err != nil {
utils.Fatalf("-netrestrict: %v", err)
}
}

addr, err := net.ResolveUDPAddr("udp", *listenAddr)
if err != nil {
utils.Fatalf("-ResolveUDPAddr: %v", err)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
utils.Fatalf("-ListenUDP: %v", err)
}

realaddr := conn.LocalAddr().(*net.UDPAddr)
if natm != nil {
if !realaddr.IP.IsLoopback() {
go nat.Map(natm, nil, "udp", realaddr.Port, realaddr.Port, "ethereum discovery")
}
if ext, err := natm.ExternalIP(); err == nil {
realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port}
}
}

if *runv5 {
if _, err := discv5.ListenUDP(nodeKey, conn, realaddr, "", restrictList); err != nil {
utils.Fatalf("%v", err)
}
} else {
cfg := discover.Config{
PrivateKey: nodeKey,
AnnounceAddr: realaddr,
NetRestrict: restrictList,
}
if _, err := discover.ListenUDP(conn, cfg); err != nil {
utils.Fatalf("%v", err)
}
}
select {}
}

从代码里可以看到,这部分功能主要来自于 p2p 模块。后续我们会深入到该模块。

clef

clef 可以用来签署交易和数据,并且可以代替 geth 的账户管理。这部分代码我没有深入研究,看起来像是账户管理的另一种封装,实际的实现在 signer 目录下。

可以参考:A Python/QT based graphical user interface for the ethereum signer

ethkey

ethkey 是一个可以用来操作以太坊 keyfile 的工具。

evm

对 evm 进行一些封装

执行合约前,将 Transaction 类型转化为 message,创建虚拟机(EVM)对象,计算一些 Gas 消耗,执行交易完毕后创建收据(Recipet)对象返回。

faucet

faucet 测试网相关代码。

参考:https://faucet.rinkeby.iohttps://coincentral.com/ethereum-faucets/

p2psim

模拟 p2p API 请求的工具

puppeth

用于搭建,维护私有链

rlpdump

RLP 是 Recursive Length Prefix 的简写。是以太坊中的序列化方法。这个模块的作用是提供 RLP 数据的格式化输出。

swarm

swarm 是一个去中心化的内容存储和分发服务,这个模块是其命令行客户端。没有细看这部分代码,swarm 看起来很像 ipfs,而且代码还在快速迭代中,就不展开了。

IPFS & SWARM

wnode

这是一个简单的 Whisper 节点。它可以用作独立的引导节点,还可以用于不同的测试和诊断目的

1
2
3
4
5
6
func main() {
processArgs()
initialize()
run()
shutdown()
}

总结

cmd 模块下的其他命令的资料相对于 geth 命令的资料还是很少的,而且有几个工具看起来有个人开发工具的性质,看起来很有用。可能使用的人很少,没法集成到 geth 成为一个子命令,但不集成为一个单独的命令又太可惜;有的工具只是某些功能的一个封装,还有的是项目外的工具的封装,例如 Solidity,Swarm。如果未来对以太坊的功能,代码比较熟悉了,需要自己封装一个工具,这些代码是很有参考价值的。

References